// Copyright Epic Games, Inc. All Rights Reserved.

#include "zipfs.h"

namespace zen {

//////////////////////////////////////////////////////////////////////////
namespace {

#if ZEN_COMPILER_MSC
#	pragma warning(push)
#	pragma warning(disable : 4200)
#endif

	using ZipInt16 = uint16_t;

	struct ZipInt32
	{
				 operator uint32_t() const { return *(uint32_t*)Parts; }
		uint16_t Parts[2];
	};

	struct EocdRecord
	{
		enum : uint32_t
		{
			Magic = 0x0605'4b50,
		};
		ZipInt32 Signature;
		ZipInt16 ThisDiskIndex;
		ZipInt16 CdStartDiskIndex;
		ZipInt16 CdRecordThisDiskCount;
		ZipInt16 CdRecordCount;
		ZipInt32 CdSize;
		ZipInt32 CdOffset;
		ZipInt16 CommentSize;
		char	 Comment[];
	};

	struct CentralDirectoryRecord
	{
		enum : uint32_t
		{
			Magic = 0x0201'4b50,
		};

		ZipInt32 Signature;
		ZipInt16 VersionMadeBy;
		ZipInt16 VersionRequired;
		ZipInt16 Flags;
		ZipInt16 CompressionMethod;
		ZipInt16 LastModTime;
		ZipInt16 LastModDate;
		ZipInt32 Crc32;
		ZipInt32 CompressedSize;
		ZipInt32 OriginalSize;
		ZipInt16 FileNameLength;
		ZipInt16 ExtraFieldLength;
		ZipInt16 CommentLength;
		ZipInt16 DiskIndex;
		ZipInt16 InternalFileAttr;
		ZipInt32 ExternalFileAttr;
		ZipInt32 Offset;
		char	 FileName[];
	};

	struct LocalFileHeader
	{
		enum : uint32_t
		{
			Magic = 0x0403'4b50,
		};

		ZipInt32 Signature;
		ZipInt16 VersionRequired;
		ZipInt16 Flags;
		ZipInt16 CompressionMethod;
		ZipInt16 LastModTime;
		ZipInt16 LastModDate;
		ZipInt32 Crc32;
		ZipInt32 CompressedSize;
		ZipInt32 OriginalSize;
		ZipInt16 FileNameLength;
		ZipInt16 ExtraFieldLength;
		char	 FileName[];
	};

#if ZEN_COMPILER_MSC
#	pragma warning(pop)
#endif

}  // namespace

//////////////////////////////////////////////////////////////////////////
ZipFs::ZipFs(IoBuffer&& Buffer)
{
	MemoryView View = Buffer.GetView();

	uint8_t* Cursor = (uint8_t*)(View.GetData()) + View.GetSize();
	if (View.GetSize() < sizeof(EocdRecord))
	{
		return;
	}

	const auto* EocdCursor = (EocdRecord*)(Cursor - sizeof(EocdRecord));

	// It is more correct to search backwards for EocdRecord::Magic as the
	// comment can be of a variable length. But here we're not going to support
	// zip files with comments.
	if (EocdCursor->Signature != EocdRecord::Magic)
	{
		return;
	}

	// Zip64 isn't supported either
	if (EocdCursor->ThisDiskIndex == 0xffff)
	{
		return;
	}

	Cursor = (uint8_t*)EocdCursor - uint32_t(EocdCursor->CdOffset) - uint32_t(EocdCursor->CdSize);

	const auto* CdCursor = (CentralDirectoryRecord*)(Cursor + EocdCursor->CdOffset);
	for (int i = 0, n = EocdCursor->CdRecordCount; i < n; ++i)
	{
		const CentralDirectoryRecord& Cd = *CdCursor;

		bool Acceptable = true;
		Acceptable &= (Cd.OriginalSize > 0);		// has some content
		Acceptable &= (Cd.CompressionMethod == 0);	// is stored uncomrpessed
		if (Acceptable)
		{
			const uint8_t* Lfh = Cursor + Cd.Offset;
			if (uintptr_t(Lfh - Cursor) < View.GetSize())
			{
				std::string_view FileName(Cd.FileName, Cd.FileNameLength);
				m_Files.insert(std::make_pair(FileName, FileItem{Lfh, size_t(0)}));
			}
		}

		uint32_t ExtraBytes = Cd.FileNameLength + Cd.ExtraFieldLength + Cd.CommentLength;
		CdCursor			= (CentralDirectoryRecord*)(Cd.FileName + ExtraBytes);
	}

	m_Buffer = std::move(Buffer);
}

//////////////////////////////////////////////////////////////////////////
IoBuffer
ZipFs::GetFile(const std::string_view& FileName) const
{
	FileMap::iterator Iter = m_Files.find(FileName);
	if (Iter == m_Files.end())
	{
		return {};
	}

	FileItem& Item = Iter->second;
	if (Item.GetSize() > 0)
	{
		return IoBuffer(IoBuffer::Wrap, Item.GetData(), Item.GetSize());
	}

	const auto* Lfh = (LocalFileHeader*)(Item.GetData());
	Item			= MemoryView(Lfh->FileName + Lfh->FileNameLength + Lfh->ExtraFieldLength, Lfh->OriginalSize);
	return IoBuffer(IoBuffer::Wrap, Item.GetData(), Item.GetSize());
}

}  // namespace zen
